The Micronaut Framework [1] is being developed by OCI [2], or more specifically under the leadership of Graeme Rocher, who has already launched the Grails Framework. Both the experience with Spring as well as with Grails have found their way into Micronaut. Therefore, the paradigms and the programming model will seem familiar to experienced Spring developers from the very start. The framework is described as a “modern, JVM-based full stack framework for building modular, easy-to-test microservices and serverless applications.” This description shows the key difference with the Spring Framework: Micronaut focuses on microservices and serverless applications – places where JVM frameworks are still having difficulty.
STAY TUNED!
Learn more about JAX London
The Disadvantages of Spring
By nature, Java applications come with some overhead. The JVM alone requires about 128 MB of RAM and 124 MB of hard disk space, according to official figures. While this is fully acceptable for traditional applications, with Docker containers in a cluster or even as a FaaS instance, such numbers no longer make the cut. To compare some figures: non-trivial applications in the Go programming language often have only 20 to 30 MB after compilation. Another important metric is the start time of an application. Start times exceeding twenty seconds are not uncommon, given Spring’s runtime reflection approach. Again, this is not acceptable for serverless applications.
What distinguishes Micronaut from Spring
Micronaut takes a different path than Spring and can therefore make up for some of the performance losses. In particular, the startup time is greatly reduced, which opens the door to the serverless world for Java developers. And RAM consumption also decreases.
How does Micronaut achieve these improvements? The answer is in the compilation. At runtime, Spring scans the classpath for beans using Reflection, initializes them, and then dynamically loads them into the application context. Then the beans are injected where they are needed. Although this is a very simple and proven approach, the overhead does extend the start time. The more classes the application contains, the more the start time suffers. Micronaut, on the other hand, uses annotation processors, which collect the necessary information at compile time and perform the necessary dependency injection (DI) and aspect-oriented programming (AOP) transformations ahead of time (AOT).
This shortens the start time of the application, but increases the compile time. In addition, this procedure prevents any errors such as an unfulfilled dependency already at compile time. In addition, the start time is not dependent on the size of the application; once it is compiled, the start time is relatively constant. Of course, the implication of this compile-time approach is that the libraries that feed into the application in addition to the framework also need to avoid reloading beans using Reflection. For example, the AOP Framework AspectJ is unsuitable for Micronaut, which is why Micronaut has provided an AOP solution itself. The following examples show how strong the improvements achieved by the framework are.
The Spring Application
Let’s use a simple application for a shopping cart as an example. The complete code is available on GitHub [3]. Products can be added to the shopping cart, queried or deleted via HTTP. First comes the Spring Boot application. To start off, you go to https://start.spring.io/ and put together a Java 8 application with Gradle, Spring Boot 2.1.2, and the web package. The archive can be unzipped anywhere on the computer.
The Java code of the application follows below. If you are familiar with Spring Boot, you should not have any problems with it. First, a controller named ShoppingCartController.java is needed.
@RestController("/shoppingCart") public class ShoppingCartController { private final ShoppingCartService shoppingCartService; public ShoppingCartController(ShoppingCartService shoppingCartService) { this.shoppingCartService = shoppingCartService; } @GetMapping public List<Product> getAllProducts() { return shoppingCartService.getAllProducts(); } @PostMapping public void addProduct(@RequestBody Product product) { shoppingCartService.addProduct(product); } @DeleteMapping public Optional<Product> deleteProduct(@RequestBody Product product) { return shoppingCartService.deleteProduct(product); } }
Next, we take a service at ShoppingCartService.java.
@Service public class ShoppingCartService { private final ArrayList<Product> products = new ArrayList<>(); public List<Product> getAllProducts() { return products; } public void addProduct(Product product) { products.add(product); } public Optional<Product> deleteProduct(Product product) { Optional<Product> result = products.stream() .filter(p -> p.getId().equals(product.getId())) .findFirst(); result.ifPresent(products::remove); return result; } }
For the sake of simplicity, the service keeps all products in a local list. We still need a POJO for the product in Product.java.
public class Product { private final Long id; private final String description; ... Constructor, Getter, Setter ... }
If the application with ./gradle bootRun has been executed, you can use a tool such as cURL to address the endpoints to test the function.
Resource consumption: You can take a look at some interesting metrics of the application in Figure 1. For the compile time, we take the the time for the bootJar Gradle task following a previous ./gradlew clean. The start time is 3.72 seconds, according to the Spring output. The actual start time additionally contains the start time of the JVM, which amounts to about 5 seconds in total.
The Micronaut Application
The previous application serves as a benchmark for the subsequent Micronaut application. The complete code is also available on GitHub [4]. Unlike Spring Boot, Micronaut comes with a command-line tool that handles the creation of projects. For the installation, please refer to the official Micronaut page [1].
Using the mn tool, you can now create the application by means of $ mn. The command starts a shell in which you have some Micronaut-specific commands available. You can create a new application in the current directory with create-app. If you enter –features = after the command and press TAB once, you get an overview of the additional features that Micronaut provides. These include the JVM languages Groovy and Kotlin, as well as several projects from the Netflix stack for microservices.
At first it seems that the default settings will be sufficient, except for one small detail: GraalVM Native Image. We will discuss what this is all about a bit later. The complete command is:
mn> create-app --features=graal-native-image com.example.myshop.shoppingcart.shopping-cart-micronaut
Enter exit to terminate the shell.
The Code
First comes the controller again, which can be created via the Micronaut shell by entering the following command:
mn> create-controller ShoppingCart
This command creates both the controller and a corresponding test and saves the programmer some time. The service bean can be created as follows:
mn> create-bean ShoppingCartService
As an advantage for Spring developers, the code of the Spring application can be copied almost one-to-one; Micronaut does not want to impose a new programming model on developers. However, the framework changes a few names of the annotations. @RestController becomes @Controller, while @GetMapping turns into @Get and so on. In ShoppingCartService, @Service becomes @Singleton. The POJO product still needs the @JsonProperty annotations of the Jackson Library (the rest of the code remains the same):
... public Product(@JsonProperty("id") Long id, @JsonProperty("description") String description) { …
Resource consumption: The numbers generated the two example applications are compared in Figure 2. This shows the improvements in Micronaut compared to Spring. While the compile time is now significantly longer, the framework can score at other metrics. It should be noted that depending on the size of the application in Spring, the start time will always be longer, while the start time of the Micronaut application will remain relatively constant.
GraalVM
The command to build the Micronaut application includes the graal-native-image feature. GraalVM [5] is a virtual machine supporting multiple languages which was developed by Oracle. It allows developers to run code from different languages within the same runtime. But that’s just the beginning: GraalVM also provides the ability to compile Java applications into native binaries. They can then be executed without JVM or GraalVM. This step is only possible if the application uses little or no reflexive reloading of classes. Micronaut is therefore very suitable for this application case.
Compiling a Micronaut application into binary format
This tool can be demonstrated on our previously created Micronaut application. This requires a GraalVM installation according to the official documentation [6]. After you have installed GraalVM, you get a “JDK replacement”. All programs like Java or javac are included and behave exactly like their original counterparts. However, in addition to the normal JDK programs, GraalVM provides a program called native-image that can take on the compilation to a native binary.
The Micronaut CLI already has the build-native-image.sh Bash script generated in the project directory. It basically contains a Gradle call to generate the JAR and call native-image. There is a downside to this procedure though: it needs a lot of RAM. If you do not provide enough RAM, the process will end with the ominous error 137; at least 16 GB of RAM should be available. The resulting binary appears in the main directory and can be conveniently started without a JVM:
$ ./shopping-cart-micronaut 14:53:31.707 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 16ms. Server Running: http://localhost:8080
The start time of 16 ms represents a significant improvement. You can take a look at the remaining metrics in Figure 3.
Understandably, the compile time is quite miserable. It’s not only the fact that Micronaut triggers the beans at compile time – the resulting Java bytecode is translated into native code in addition. The advantage for developers: you actually never need to perform the step locally. While you can also locally use the Java version for testing, only the build server performs the time consuming compilation step. Also, the size difference is not really an issue. After all, the JAR itself is only 11.3 MB, but you still need a JRE for this, which consumes space again. The binary also works without a JRE and can be provided individually or within a minimal Docker image. Especially the low RAM consumption shows how valuable the approach can be for the serverless world, in which every megabyte of RAM costs money.
Conclusion
The fledgling Micronaut framework provides Java developers with the ability to write lean and fast cloud applications without having to sacrifice the trusted programming model provided by Spring. So should Java developers write off Spring and use Micronaut? In my opinion, things haven’t gone that far yet. When deciding which framework to use for a larger application, it’s not just performance we need to look at. The community and teaching materials must be consistent, and at this point Micronaut is (still) lagging behind. Most projects available on GitHub are smaller example applications. So it’s still unclear how the framework behaves in a real application.
Nevertheless, Micronaut is still worth looking into for small applications, especially in the serverless environment, so often mentioned. Last but not least, competition is good for the market. Maybe a few ideas by Micronaut developers will find their way into the Spring Framework.
Links
[2] https://objectcomputing.com
[3] https://github.com/t-buss/shopping-cart/tree/master/shopping-cart-spring/
[4] https://github.com/t-buss/shopping-cart/tree/master/shopping-cart-micronaut/